﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace HashFoo.Rest.Common
{
    /// <summary>
    /// Builder to help create resource to uri mappings.
    /// </summary>
    public class ResourceUriCollectionBuilder
    {
        private readonly IDictionary<string, string> _assemblyRootNamespaces = new Dictionary<string, string>();
        private readonly Stack<ResourceUri> _explicitResourceUriMaps = new Stack<ResourceUri>();
        private readonly Stack<Type> _resources = new Stack<Type>();
        private readonly List<string> _knownSuffixes = new List<string>();

        private string _rootNamespace = string.Empty;
        private string _uriPrefix = string.Empty;
        private bool _ignoreKnownSuffix = true;

        /// <summary>
        /// Takes a resource type and finds the root namespace for that type.
        /// </summary>
        public Func<Type, string> ResourceRootNamespaceFinder { get; set; }

        /// <summary>
        /// Takes a resource type, and finds, if it exists, the resource root namespace attribute override on the assembly.
        /// </summary>
        public Func<Assembly, ResourceRootNamespaceAttribute> ResourceRootNamespaceAttributeFinder { get; set; }

        /// <summary>
        /// Create a 
        /// </summary>
        public ResourceUriCollectionBuilder()
        {
            // lazy man's DI
            ResourceRootNamespaceFinder = GetDefaultRootNamespace;
            ResourceRootNamespaceAttributeFinder = GetRootNamespaceAttribute;
        }

        /// <summary>
        /// Add explicit resource uri mappings.  Added last to the beginning of the collection (highest priority).
        /// </summary>
        /// <param name="resourceUris"></param>
        /// <returns></returns>
        public ResourceUriCollectionBuilder AddExplicit(params ResourceUri[] resourceUris)
        {
            foreach (var resourceUri in resourceUris) _explicitResourceUriMaps.Push(resourceUri);
            return this;
        }

        /// <summary>
        /// Adds all <see cref="IResource"/> from the assembly that contains T.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public ResourceUriCollectionBuilder AddFromAssemblyOf<T>() where T : IResource
        {
            var assembly = typeof (T).Assembly;
            var resources =
                assembly.GetTypes().Where(
                    t => typeof (IResource).IsAssignableFrom(t) && t.IsAbstract == false && t.IsInterface == false);

            foreach (var resource in resources) _resources.Push(resource);
            return this;
        }

        /// <summary>
        /// Adds a known suffix for command and query objects.  When building uris, a uri without the suffix will be created.
        /// </summary>
        /// <param name="suffix">The resource suffix ("Command" and "Query")</param>
        /// <returns></returns>
        public ResourceUriCollectionBuilder AddKnownSuffix(string suffix)
        {
            if (_knownSuffixes.Any(s => s == suffix) == false) _knownSuffixes.Add(suffix);

            return this;
        }

        /// <summary>
        /// If true, will create resource uris without the known suffix.
        /// Such as: '/logincommand' will become '/login'.
        /// </summary>
        /// <param name="ignore"></param>
        /// <returns></returns>
        public ResourceUriCollectionBuilder IgnoreKnownSuffix(bool ignore)
        {
            _ignoreKnownSuffix = ignore;
            return this;
        }

        /// <summary>
        /// Builds the <see cref="IResourceUriCollection"/>
        /// </summary>
        /// <returns></returns>
        public IResourceUriCollection Build()
        {
            return new ResourceUriCollection(
                _explicitResourceUriMaps.Concat(
                    _resources.Select(resource => new ResourceUri {ResourceType = resource, Uri = BuildUri(resource)}))
                );
        }

        /// <summary>
        /// Overrides the root namespace for all <see cref="IResource"/>s in this builder.
        /// </summary>
        /// <param name="rootNamespace"></param>
        /// <returns></returns>
        public ResourceUriCollectionBuilder RootNamespace(string rootNamespace)
        {
            _rootNamespace = rootNamespace;
            return this;
        }

        /// <summary>
        /// Adds a prefix to all the generated <see cref="ResourceUri"/>s.
        /// </summary>
        /// <param name="uriPrefix"></param>
        /// <returns></returns>
        public ResourceUriCollectionBuilder UriPrefix(string uriPrefix)
        {
            _uriPrefix = uriPrefix;
            return this;
        }

        /// <summary>
        /// Builds the uri for a specific resource type.
        /// </summary>
        /// <param name="resourceType"></param>
        /// <returns></returns>
        protected virtual string BuildUri(Type resourceType)
        {
            if (resourceType.Namespace == null)
                throw new InvalidOperationException("Could not find namespace of reference type: " + resourceType);

            var rootNamespace = ResourceRootNamespaceFinder(resourceType);
            var uriNamespace = resourceType.Namespace.Substring(rootNamespace.Length + 1);

            var prefix = string.IsNullOrEmpty(_uriPrefix)
                             ? string.Empty
                             : _uriPrefix.EndsWith("/") ? _uriPrefix : _uriPrefix + "/";

            var uri = prefix + (uriNamespace.Replace('.', '/') + "/" + resourceType.Name).ToLowerInvariant();

            if (!_ignoreKnownSuffix) return uri;

            // remove any known suffix
            var knownSuffix = _knownSuffixes.FirstOrDefault(s => uri.Contains(s.ToLowerInvariant()));
            if (knownSuffix != null)
            {
                var suffixIndex = uri.IndexOf(knownSuffix.ToLowerInvariant(), StringComparison.Ordinal);
                // only remove the suffix if its really at the end.
                if ((suffixIndex + knownSuffix.Length) == uri.Length)
                {
                    uri = uri.Substring(0, suffixIndex);
                }
            }

            return uri;
        }

        private string GetDefaultRootNamespace(Type resourceType)
        {
            if (resourceType.Namespace == null)
                throw new InvalidOperationException("Could not find namespace of reference type: " + resourceType);

            if (string.IsNullOrEmpty(_rootNamespace) == false) return _rootNamespace;

            var assembly = resourceType.Assembly;
            var assemblyName = assembly.FullName;
            if (_assemblyRootNamespaces.ContainsKey(assemblyName)) return _assemblyRootNamespaces[assemblyName];

            var rootNamespaceAttribute = ResourceRootNamespaceAttributeFinder(assembly);

            var rootNamespace =
                rootNamespaceAttribute == null
                    ? resourceType.Namespace.Substring(0, resourceType.Namespace.IndexOf('.'))
                    : rootNamespaceAttribute.RootNamespace;

            // cache for next time
            _assemblyRootNamespaces[assemblyName] = rootNamespace;
            return rootNamespace;
        }

        private static ResourceRootNamespaceAttribute GetRootNamespaceAttribute(Assembly assembly)
        {
            var rootNamespaceAttribute =
                assembly
                    .GetCustomAttributes(typeof (ResourceRootNamespaceAttribute), false)
                    .Cast<ResourceRootNamespaceAttribute>().FirstOrDefault();
            return rootNamespaceAttribute;
        }
    }
}